/*
 * Copyright 2008 Sun Microsystems, Inc.  All rights reserved.
 * Use is subject to license terms.
 */


#include "prof_int.h"

#include <stdio.h>
#include <string.h>
#ifdef HAVE_STDLIB_H
#include <stdlib.h>
#endif
#include <errno.h>
#include <ctype.h>

#define SECTION_SEP_CHAR '/'

#define STATE_INIT_COMMENT	1
#define STATE_STD_LINE		2
#define STATE_GET_OBRACE	3

struct parse_state {
	int	state;
	int	group_level;
	struct profile_node *root_section;
	struct profile_node *current_section;
};

static char *skip_over_blanks(char *cp)
{
	while (*cp && isspace((int) (*cp)))
		cp++;
	return cp;
}

static void strip_line(char *line)
{
	char *p = line + strlen(line);
	while (p > line && (p[-1] == '\n' || p[-1] == '\r'))
	    *p-- = 0;
}

static void parse_quoted_string(char *str)
{
	char *to, *from;

	to = from = str;

	for (to = from = str; *from && *from != '"'; to++, from++) {
		if (*from == '\\') {
			from++;
			switch (*from) {
			case 'n':
				*to = '\n';
				break;
			case 't':
				*to = '\t';
				break;
			case 'b':
				*to = '\b';
				break;
			default:
				*to = *from;
			}
			continue;
		}
		*to = *from;
	}
	*to = '\0';
}


static errcode_t parse_init_state(struct parse_state *state)
{
	state->state = STATE_INIT_COMMENT;
	state->group_level = 0;

	return profile_create_node("(root)", 0, &state->root_section);
}

static errcode_t parse_std_line(char *line, struct parse_state *state)
{
	char	*cp, ch, *tag, *value;
	char	*p;
	errcode_t retval;
	struct profile_node	*node;
	int do_subsection = 0;
	void *iter = 0;

	if (*line == 0)
		return 0;
	cp = skip_over_blanks(line);
	if (cp[0] == ';' || cp[0] == '#')
		return 0;
	strip_line(cp);
	ch = *cp;
	if (ch == 0)
		return 0;
	if (ch == '[') {
		if (state->group_level > 0)
			return PROF_SECTION_NOTOP;
		cp++;
		p = strchr(cp, ']');
		if (p == NULL)
			return PROF_SECTION_SYNTAX;
		*p = '\0';
		retval = profile_find_node_subsection(state->root_section,
						 cp, &iter, 0,
						 &state->current_section);
		if (retval == PROF_NO_SECTION) {
			retval = profile_add_node(state->root_section,
						  cp, 0,
						  &state->current_section);
			if (retval)
				return retval;
		} else if (retval)
			return retval;

		/*
		 * Finish off the rest of the line.
		 */
		cp = p+1;
		if (*cp == '*') {
			profile_make_node_final(state->current_section);
			cp++;
		}
		/*
		 * A space after ']' should not be fatal
		 */
		cp = skip_over_blanks(cp);
		if (*cp)
			return PROF_SECTION_SYNTAX;
		return 0;
	}
	if (ch == '}') {
		if (state->group_level == 0)
			return PROF_EXTRA_CBRACE;
		if (*(cp+1) == '*')
			profile_make_node_final(state->current_section);
		retval = profile_get_node_parent(state->current_section,
						 &state->current_section);
		if (retval)
			return retval;
		state->group_level--;
		return 0;
	}
	/*
	 * Parse the relations
	 */
	tag = cp;
	cp = strchr(cp, '=');
	if (!cp)
		return PROF_RELATION_SYNTAX;
	if (cp == tag)
	    return PROF_RELATION_SYNTAX;
	*cp = '\0';
	p = tag;
	/* Look for whitespace on left-hand side.  */
	while (p < cp && !isspace((int)*p))
	    p++;
	if (p < cp) {
	    /* Found some sort of whitespace.  */
	    *p++ = 0;
	    /* If we have more non-whitespace, it's an error.  */
	    while (p < cp) {
		if (!isspace((int)*p))
		    return PROF_RELATION_SYNTAX;
		p++;
	    }
	}
	cp = skip_over_blanks(cp+1);
	value = cp;
	if (value[0] == '"') {
		value++;
		parse_quoted_string(value);
	} else if (value[0] == 0) {
		do_subsection++;
		state->state = STATE_GET_OBRACE;
	} else if (value[0] == '{' && *(skip_over_blanks(value+1)) == 0)
		do_subsection++;
	else {
		cp = value + strlen(value) - 1;
		while ((cp > value) && isspace((int) (*cp)))
			*cp-- = 0;
	}
	if (do_subsection) {
		p = strchr(tag, '*');
		if (p)
			*p = '\0';
		retval = profile_add_node(state->current_section,
					  tag, 0, &state->current_section);
		if (retval)
			return retval;
		if (p)
			profile_make_node_final(state->current_section);
		state->group_level++;
		return 0;
	}
	p = strchr(tag, '*');
	if (p)
		*p = '\0';
	profile_add_node(state->current_section, tag, value, &node);
	if (p)
		profile_make_node_final(node);
	return 0;
}

static errcode_t parse_line(char *line, struct parse_state *state)
{
	char	*cp;

	switch (state->state) {
	case STATE_INIT_COMMENT:
		if (line[0] != '[')
			return 0;
		state->state = STATE_STD_LINE;
		/*FALLTHRU*/
	case STATE_STD_LINE:
		return parse_std_line(line, state);
	case STATE_GET_OBRACE:
		cp = skip_over_blanks(line);
		if (*cp != '{')
			return PROF_MISSING_OBRACE;
		state->state = STATE_STD_LINE;
		/*FALLTHRU*/
	}
	return 0;
}

errcode_t profile_parse_file(FILE *f, struct profile_node **root)
{
#define BUF_SIZE	2048
	char *bptr;
	errcode_t retval;
	struct parse_state state;

	bptr = malloc (BUF_SIZE);
	if (!bptr)
		return ENOMEM;

	retval = parse_init_state(&state);
	if (retval) {
		free (bptr);
		return retval;
	}
	while (!feof(f)) {
		if (fgets(bptr, BUF_SIZE, f) == NULL)
			break;
#ifndef PROFILE_SUPPORTS_FOREIGN_NEWLINES
		retval = parse_line(bptr, &state);
		if (retval) {
			/* Solaris Kerberos: check if an unconfigured file */
			if (strstr(bptr, "___"))
				retval = PROF_NO_PROFILE;
			free (bptr);
			return retval;
		}
#else
		{
		    char *p, *end;

		    if (strlen(bptr) >= BUF_SIZE - 1) {
			/* The string may have foreign newlines and
			   gotten chopped off on a non-newline
			   boundary.  Seek backwards to the last known
			   newline.  */
			long offset;
			char *c = bptr + strlen (bptr);
			for (offset = 0; offset > -BUF_SIZE; offset--) {
			    if (*c == '\r' || *c == '\n') {
				*c = '\0';
				fseek (f, offset, SEEK_CUR);
				break;
			    }
			    c--;
			}
		    }

		    /* First change all newlines to \n */
		    for (p = bptr; *p != '\0'; p++) {
			if (*p == '\r')
                            *p = '\n';
		    }
		    /* Then parse all lines */
		    p = bptr;
		    end = bptr + strlen (bptr);
		    while (p < end) {
			char* newline;
			char* newp;

			newline = strchr (p, '\n');
			if (newline != NULL)
			    *newline = '\0';

			/* parse_line modifies contents of p */
			newp = p + strlen (p) + 1;
			retval = parse_line (p, &state);
			if (retval) {
			    free (bptr);
			    return retval;
			}

			p = newp;
		    }
		}
#endif
	}
	*root = state.root_section;

	free (bptr);
	return 0;
}

/*
 * Return TRUE if the string begins or ends with whitespace
 */
static int need_double_quotes(char *str)
{
	if (!str)
                return 0;
	if (str[0] == '\0')
		return 1;
	if (isspace((int) (*str)) ||isspace((int) (*(str + strlen(str) - 1))))
		return 1;
	if (strchr(str, '\n') || strchr(str, '\t') || strchr(str, '\b'))
		return 1;
	return 0;
}

/*
 * Output a string with double quotes, doing appropriate backquoting
 * of characters as necessary.
 */
static void output_quoted_string(char *str, void (*cb)(const char *,void *),
				 void *data)
{
	char	ch;
	char buf[2];

	cb("\"", data);
	if (!str) {
		cb("\"", data);
		return;
	}
	buf[1] = 0;
	while ((ch = *str++)) {
		switch (ch) {
		case '\\':
			cb("\\\\", data);
			break;
		case '\n':
			cb("\\n", data);
			break;
		case '\t':
			cb("\\t", data);
			break;
		case '\b':
			cb("\\b", data);
			break;
		default:
			/* This would be a lot faster if we scanned
			   forward for the next "interesting"
			   character.  */
			buf[0] = ch;
			cb(buf, data);
			break;
		}
	}
	cb("\"", data);
}



#if defined(_WIN32)
#define EOL "\r\n"
#endif

#ifndef EOL
#define EOL "\n"
#endif

/* Errors should be returned, not ignored!  */
static void dump_profile(struct profile_node *root, int level,
			 void (*cb)(const char *, void *), void *data)
{
	int i;
	struct profile_node *p;
	void *iter;
	long retval;
	char *name, *value;

	iter = 0;
	do {
		retval = profile_find_node_relation(root, 0, &iter,
						    &name, &value);
		if (retval)
			break;
		for (i=0; i < level; i++)
			cb("\t", data);
		if (need_double_quotes(value)) {
			cb(name, data);
			cb(" = ", data);
			output_quoted_string(value, cb, data);
			cb(EOL, data);
		} else {
			cb(name, data);
			cb(" = ", data);
			cb(value, data);
			cb(EOL, data);
		}
	} while (iter != 0);

	iter = 0;
	do {
		retval = profile_find_node_subsection(root, 0, &iter,
						      &name, &p);
		if (retval)
			break;
		if (level == 0)	{ /* [xxx] */
			cb("[", data);
			cb(name, data);
			cb("]", data);
			cb(profile_is_node_final(p) ? "*" : "", data);
			cb(EOL, data);
			dump_profile(p, level+1, cb, data);
			cb(EOL, data);
		} else { 	/* xxx = { ... } */
			for (i=0; i < level; i++)
				cb("\t", data);
			cb(name, data);
			cb(" = {", data);
			cb(EOL, data);
			dump_profile(p, level+1, cb, data);
			for (i=0; i < level; i++)
				cb("\t", data);
			cb("}", data);
			cb(profile_is_node_final(p) ? "*" : "", data);
			cb(EOL, data);
		}
	} while (iter != 0);
}

static void dump_profile_to_file_cb(const char *str, void *data)
{
	fputs(str, data);
}

errcode_t profile_write_tree_file(struct profile_node *root, FILE *dstfile)
{
	dump_profile(root, 0, dump_profile_to_file_cb, dstfile);
	return 0;
}

struct prof_buf {
	char *base;
	size_t cur, max;
	int err;
};

static void add_data_to_buffer(struct prof_buf *b, const void *d, size_t len)
{
	if (b->err)
		return;
	if (b->max - b->cur < len) {
		size_t newsize;
		char *newptr;

		newsize = b->max + (b->max >> 1) + len + 1024;
		newptr = realloc(b->base, newsize);
		if (newptr == NULL) {
			b->err = 1;
			return;
		}
		b->base = newptr;
		b->max = newsize;
	}
	memcpy(b->base + b->cur, d, len);
	b->cur += len; 		/* ignore overflow */
}

static void dump_profile_to_buffer_cb(const char *str, void *data)
{
	add_data_to_buffer((struct prof_buf *)data, str, strlen(str));
}

errcode_t profile_write_tree_to_buffer(struct profile_node *root,
				       char **buf)
{
	struct prof_buf prof_buf = { 0, 0, 0, 0 };

	dump_profile(root, 0, dump_profile_to_buffer_cb, &prof_buf);
	if (prof_buf.err) {
		*buf = NULL;
		return ENOMEM;
	}
	add_data_to_buffer(&prof_buf, "", 1); /* append nul */
	if (prof_buf.max - prof_buf.cur > (prof_buf.max >> 3)) {
		char *newptr = realloc(prof_buf.base, prof_buf.cur);
		if (newptr)
			prof_buf.base = newptr;
	}
	*buf = prof_buf.base;
	return 0;
}
